Initial product import
Business scenario
When a merchant begins using Qoyod, their existing product catalog — potentially hundreds or thousands of items already managed in an e-commerce platform, POS system, or ERP — does not automatically appear in Qoyod. Before any invoice can be issued, any stock can be tracked, or any cost posting can occur, each product must exist as a Qoyod record with a numeric ID.
This use case covers a one-time, batch migration: the integrated system sends its entire existing product catalog to Qoyod in a single operation (or a series of chunks), rather than waiting for individual product-creation events to occur. The result is a populated Qoyod product catalog that the integration can reference immediately for invoices, inventory tracking, and accounting entries.
If this migration is skipped or only partially completed, the catalog in Qoyod will be incomplete. Subsequent invoice creation will fail for any product not yet imported. Stock tracking cannot begin. The gap compounds over time as transactions accumulate against products that Qoyod has no record of, requiring progressively more remediation work to backfill.
When to use this
Use this when:
- You are starting a new Qoyod integration and need to populate Qoyod with products that already exist in your integrated system before go-live.
- Your catalog contains multiple products and you want to import them in bulk rather than one at a time.
- You are performing a one-time migration from a legacy system where no ongoing per-event creation flow exists yet.
Do not use this when:
- A single new product has been added to your integrated system and you need to create it in Qoyod in response to that event — that is New product creation (UC-01).
- The product already exists in Qoyod and you need to update its fields — that is Product update (UC-02).
- You need to set or adjust stock quantities after the import — stock levels are managed through inventory adjustments, not through product creation.
- You need to create categories before importing products — complete Category management (UC-06) first.
Prerequisites
- A valid OAuth 2.0 access token, or an API key, for the Qoyod account you are integrating with.
- All categories you intend to assign to imported products must already exist in Qoyod. Retrieve their integer
idvalues before running this import. See Category management (UC-06). - All tax records you intend to assign must already exist in Qoyod. Retrieve their integer
idvalues before running this import. - A reliable mechanism in your integration to capture and durably store the Qoyod product
idfor each imported item. These IDs are required for all future updates and references. - Your product catalog prepared and deduplicated on your side. Qoyod does not enforce uniqueness on
skuoren_name— duplicate submissions result in duplicate records.
Sequence diagram
Step-by-step
1. Prepare and chunk the catalog
Export your full product catalog from the integrated system and group the records into chunks. Qoyod does not document a maximum batch size.
Start with chunks of 50 products and increase the chunk size only after confirming stability. For each product, resolve before building the request: the Qoyod category_id, the Qoyod tax_id, and the correct type value (Product, Service, Expense, RawMaterial, or Recipe). The type field is set once at creation and cannot be changed via the API.
2. Ensure categories exist
Before sending the first batch, verify every category your products reference exists in Qoyod. See Category management (UC-06). Products referencing a non-existent category_id will fail with 422 Unprocessable Entity.
3. Send each chunk as a batch request
Wrap your array of product objects under the product key. When product is an array, Qoyod returns 200 OK with a products array — not 201 Created.
{
"product": [
{
"en_name": "Blue Ceramic Mug",
"name": "كوب سيراميك أزرق",
"type": "Product",
"sku": "MUG-BLUE-001",
"selling_price": 25.00,
"buying_price": 12.00,
"track_quantity": 1,
"category_id": 42
},
{
"en_name": "Linen Napkin Set",
"name": "طقم مناديل كتان",
"type": "Product",
"sku": "NAP-LIN-004",
"selling_price": 18.00,
"buying_price": 7.50,
"track_quantity": 1,
"category_id": 42
}
]
}
curl -X POST https://api.qoyod.com/2.0/products \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{ "product": [ ... ] }'
curl -X POST https://api.qoyod.com/2.0/products \
-H "API-KEY: {api_key}" \
-H "Content-Type: application/json" \
-d '{ "product": [ ... ] }'
4. Handle the batch response shape
Batch returns 200 OK with a products array. Single-object creation returns 201 Created with a product object. Handle both shapes if your chunk size may reach 1.
{
"products": [
{
"id": 1087,
"name_en": "Blue Ceramic Mug",
"name_ar": "كوب سيراميك أزرق",
"type": "Product",
"sku": "MUG-BLUE-001",
"selling_price": 25.0,
"track_quantity": 1,
"category_id": 42,
"created_at": "2026-05-18T08:00:00.000Z"
}
]
}
:::warning Limitation
Per-item partial failure behavior in batch mode, you've to compare the count of objects in the products response array against the count you sent. If fewer items returned, compare sku values to identify which were not created.
:::
5. Store returned IDs
For each object in the products array, store the id against the corresponding record in your integrated system. Match by sku — do not rely on positional correspondence.
6. Verify the import
After all chunks have been sent, query GET /2.0/products to confirm the total count.
curl -X GET "https://api.qoyod.com/2.0/products?per_page=1&page=1" \
-H "Authorization: Bearer {access_token}"
Check pagination.total against your expected product count.
Field mapping
The individual field table for POST /2.0/products is in New product creation — Field mapping. Each item in the batch array uses exactly the same field set. The batch-specific differences are:
- Request wrapper:
productis an array of objects, not a single object. - Response key: the response body contains
products(array), notproduct(object). - Status code: batch creation returns
200 OK; single-object creation returns201 Created. Your HTTP client must not treat200as a failure for this endpoint.
Fields with particular importance for a bulk import:
| Field | Type | Notes for bulk import |
|---|---|---|
type | string (enum) | Set once. Cannot be changed after creation. Resolve before building each batch item. Valid values: Product, Service, Expense, RawMaterial, Recipe. |
track_quantity | integer | Defaults to 0 if omitted. Set to 1 explicitly for every product whose stock level Qoyod should track. |
category_id | integer | Must reference an existing Qoyod category. Resolve all category IDs before the first batch request. |
tax_id | integer | Must reference an existing Qoyod tax record. Resolve all tax IDs before the first batch request. |
sku | string | Not enforced as unique by Qoyod. Use sku as your matching key when pairing response items to your local records. |
For the full schema, see the products reference in the Scalar API reference.
Verification
After the import run completes, verify the result at two levels.
Count check. Send a GET /2.0/products request with per_page=1 and read pagination.total. Compare against the number of products you intended to import. If the counts differ, some items were not created — investigate using the SKU-based approach below.
curl -X GET "https://api.qoyod.com/2.0/products?per_page=1&page=1" \
-H "Authorization: Bearer {access_token}"
SKU spot-check. For a representative sample of imported products, use the q[sku_eq] filter to retrieve the product by its SKU and confirm the Qoyod id stored in your integrated system matches the record returned.
curl -X GET "https://api.qoyod.com/2.0/products?q%5Bsku_eq%5D=MUG-BLUE-001" \
-H "Authorization: Bearer {access_token}"
Individual record check. For products where you already stored the Qoyod id, confirm the record exists and the key fields are correct by fetching GET /2.0/products/{id}.
curl -X GET "https://api.qoyod.com/2.0/products/1087" \
-H "Authorization: Bearer {access_token}"
Confirm the returned type, track_quantity, category_id, and selling_price match what you sent. Any discrepancy indicates the batch item was accepted with different data than expected.
Error scenarios
The following errors are realistic for this flow. For the full error catalog, see Error handling.
422 Unprocessable Entity
Returned when one or more product objects in the batch fail validation. The response body contains an errors object where keys are field names and values are arrays of error message strings.
{
"errors": {
"category_id": ["does not exist"],
"en_name": ["can't be blank"]
}
}
Common causes in a bulk import:
- A
category_idortax_idreferences a record that does not exist in Qoyod — resolve all foreign-key IDs before sending. - A required field is missing or null — check that
en_nameornameis present for each item.
401 Unauthorized
Returned when the access token or API key is missing, expired, or invalid. Refresh the access token and retry the request. The 401 response body schema is not documented in the spec.
Batch count mismatch (no error status returned)
If the products array in a 200 OK response contains fewer items than you sent, one or more items were silently dropped or rejected without a top-level error status. Compare the sku values in the response against those you sent to identify which were not created, then investigate and resend only the missing items.